﻿using Newtonsoft.Json.Linq;
using Sidvall.Net;
using Sidvall.Serialization;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Sidvall.Security
{
    public class AuthenticationClient : IAuthenticationClient
    {
        #region Public Members

        #region CreateAuthorizeAddress

        public string CreateAuthorizeAddress(string clientId, string responseType, string scope, string redirectAddress, string state, string nonce, string logOnHint,
            string authenticationContextReferences, string prompt, string responseMode, string codeChallenge, string codeChallengeMethod, Dictionary<string, string> additionalValues)
        {
            var values = new Dictionary<string, string>
            {
                { AuthenticationConstants.ClientId, clientId },
                { AuthenticationConstants.ResponseType, responseType }
            };
            if (!string.IsNullOrWhiteSpace(scope))
            {
                values.Add(AuthenticationConstants.Scope, scope);
            }
            if (!string.IsNullOrWhiteSpace(redirectAddress))
            {
                values.Add(AuthenticationConstants.RedirectUri, redirectAddress);
            }
            if (!string.IsNullOrWhiteSpace(state))
            {
                values.Add(AuthenticationConstants.State, state);
            }
            if (!string.IsNullOrWhiteSpace(nonce))
            {
                values.Add(AuthenticationConstants.Nonce, nonce);
            }
            if (!string.IsNullOrWhiteSpace(logOnHint))
            {
                values.Add(AuthenticationConstants.LogOnHint, logOnHint);
            }
            if (!string.IsNullOrWhiteSpace(authenticationContextReferences))
            {
                values.Add(AuthenticationConstants.AuthenticationContextReferences, authenticationContextReferences);
            }
            if (!string.IsNullOrWhiteSpace(prompt))
            {
                values.Add(AuthenticationConstants.Prompt, prompt);
            }
            if (!string.IsNullOrWhiteSpace(responseMode))
            {
                values.Add(AuthenticationConstants.ResponseMode, responseMode);
            }
            if (!string.IsNullOrWhiteSpace(codeChallenge))
            {
                values.Add(AuthenticationConstants.CodeChallenge, codeChallenge);
            }
            if (!string.IsNullOrWhiteSpace(codeChallengeMethod))
            {
                values.Add(AuthenticationConstants.CodeChallengeMethod, codeChallengeMethod);
            }
            return CreateWebAddress(this.Address, Merge(values, additionalValues));
        }

        #endregion
        #region CreateLogOffAddress

        public string CreateLogOffAddress(string identityToken, string redirectAddress)
        {
            var values = new Dictionary<string, string>
            {
                { AuthenticationConstants.IdentityTokenHint, identityToken }
            };
            if (!string.IsNullOrWhiteSpace(redirectAddress))
            {
                values.Add(AuthenticationConstants.PostLogOffRedirectUri, redirectAddress);
            }
            return CreateWebAddress(this.Address, values);
        }

        #endregion

        #region RequestAssertionAsync

        public async Task<TokenResponse> RequestAssertionAsync(string assertionType, string assertion, string scope, Dictionary<string, string> additionalValues,
            CancellationToken cancellationToken)
        {
            var fields = new Dictionary<string, string>
            {
                { AuthenticationConstants.GrantType, assertionType},
                { AuthenticationConstants.Assertion, assertion }
            };
            if (!string.IsNullOrWhiteSpace(scope))
            {
                fields.Add(AuthenticationConstants.Scope, scope);
            }
            return await RequestAsync(Merge(fields, additionalValues), cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region RequestAuthorizationCodeAsync

        public async Task<TokenResponse> RequestAuthorizationCodeAsync(string code, string redirectAddress, string codeVerifier, Dictionary<string, string> additionalValues,
            CancellationToken cancellationToken)
        {
            var fields = new Dictionary<string, string>
            {
                { AuthenticationConstants.GrantType, GrantTypes.AuthorizationCode },
                { AuthenticationConstants.Code, code },
                { AuthenticationConstants.RedirectUri, redirectAddress }
            };
            if (!string.IsNullOrWhiteSpace(codeVerifier))
            {
                fields.Add(AuthenticationConstants.CodeVerifier, codeVerifier);
            }
            return await RequestAsync(Merge(fields, additionalValues), cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region RequestClientCredentialsAsync

        public async Task<TokenResponse> RequestClientCredentialsAsync(string scope, Dictionary<string, string> additionalValues,
            CancellationToken cancellationToken)
        {
            var fields = new Dictionary<string, string>
            {
                { AuthenticationConstants.GrantType, GrantTypes.ClientCredentials }
            };
            if (!string.IsNullOrWhiteSpace(scope))
            {
                fields.Add(AuthenticationConstants.Scope, scope);
            }
            return await RequestAsync(Merge(fields, additionalValues), cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region RequestCustomAsync

        public async Task<TokenResponse> RequestCustomAsync(Dictionary<string, string> values,
            CancellationToken cancellationToken)
        {
            return await RequestAsync(Merge(values, null), cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region RequestCustomGrantAsync

        public async Task<TokenResponse> RequestCustomGrantAsync(string grantType, string scope, Dictionary<string, string> additionalValues,
            CancellationToken cancellationToken)
        {
            var fields = new Dictionary<string, string>
            {
                { AuthenticationConstants.GrantType, grantType}
            };
            if (!string.IsNullOrWhiteSpace(scope))
            {
                fields.Add(AuthenticationConstants.Scope, scope);
            }
            return await RequestAsync(Merge(fields, additionalValues), cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region RequestRefreshTokenAsync

        public async Task<TokenResponse> RequestRefreshTokenAsync(string refreshToken, Dictionary<string, string> additionalValues,
            CancellationToken cancellationToken)
        {
            var fields = new Dictionary<string, string>
            {
                { AuthenticationConstants.GrantType, GrantTypes.RefreshToken },
                { AuthenticationConstants.RefreshToken, refreshToken }
            };
            return await RequestAsync(Merge(fields, additionalValues), cancellationToken).ConfigureAwait(false);
        }

        #endregion
        #region RequestResourceOwnerPasswordAsync

        public async Task<TokenResponse> RequestResourceOwnerPasswordAsync(string userName, string password, string scope, Dictionary<string, string> additionalValues,
            CancellationToken cancellationToken)
        {
            var fields = new Dictionary<string, string>
            {
                { AuthenticationConstants.GrantType, GrantTypes.Password },
                { AuthenticationConstants.UserName, userName },
                { AuthenticationConstants.Password, password }
            };
            if (!string.IsNullOrWhiteSpace(scope))
            {
                fields.Add(AuthenticationConstants.Scope, scope);
            }
            return await RequestAsync(Merge(fields, additionalValues), cancellationToken).ConfigureAwait(false);
        }

        #endregion

        #endregion
        #region Private Members

        private IHttpClient Client { get; set; }
        private ClientAuthenticationStyle AuthenticationStyle { get; set; }
        private System.Uri Address { get; set; }
        private string ClientId { get; set; }
        private string ClientSecret { get; set; }

        #region CreateWebAddress

        private static string CreateWebAddress(System.Uri endpoint, Dictionary<string, string> values)
        {
            var qs = string.Join("&", values.Select(kvp => System.String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}={1}", System.Net.WebUtility.UrlEncode(kvp.Key), System.Net.WebUtility.UrlEncode(kvp.Value))).ToArray());
            return string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}?{1}", endpoint.AbsoluteUri, qs);
        }

        #endregion
        #region GetTokenResponse

        private static TokenResponse GetTokenResponse(System.Net.HttpStatusCode statusCode, string reason)
        {
            return new TokenResponse()
            {
                HttpErrorReason = reason,
                HttpErrorStatusCode = statusCode,
                IsError = true,
                IsHttpError = true,
            };
        }

        #endregion
        #region GetTokenResponseFromContent

        private static TokenResponse GetTokenResponseFromContent(System.Net.HttpStatusCode statusCode, string json)
        {
            var item = JObject.Parse(json);
            var tokenResponse = new TokenResponse()
            {
                AccessToken = item.ValueToString(AuthenticationConstants.AccessToken),
                Error = item.ValueToString(AuthenticationConstants.Error),
                ExpiresIn = item.ValueToString(AuthenticationConstants.ExpiresIn).ToInt64(0),
                HttpErrorReason = null,
                HttpErrorStatusCode = statusCode,
                IdentityToken = item.ValueToString(AuthenticationConstants.IdentityToken),
                IsHttpError = false,
                RefreshToken = item.ValueToString(AuthenticationConstants.RefreshToken),
                TokenType = item.ValueToString(AuthenticationConstants.TokenType),
            };
            tokenResponse.IsError = !string.IsNullOrWhiteSpace(tokenResponse.Error);
            return tokenResponse;
        }

        #endregion
        #region Merge

        private Dictionary<string, string> Merge(Dictionary<string, string> explicitValues, Dictionary<string, string> additionalValues)
        {
            var merged = explicitValues;
            if (this.AuthenticationStyle == ClientAuthenticationStyle.PostValues)
            {
                merged.Add(AuthenticationConstants.ClientId, this.ClientId);
                merged.Add(AuthenticationConstants.ClientSecret, this.ClientSecret);
            }
            if (additionalValues != null)
            {
                merged = explicitValues.Concat(additionalValues.Where(add => !explicitValues.ContainsKey(add.Key))).ToDictionary(final => final.Key, final => final.Value);
            }
            return merged;
        }

        #endregion
        #region RequestAsync

        private async Task<TokenResponse> RequestAsync(Dictionary<string, string> form, CancellationToken cancellationToken)
        {
            var response = await this.Client.PostAsync(string.Empty, form, cancellationToken).ConfigureAwait(false);
            if (response.StatusCode == System.Net.HttpStatusCode.OK || response.StatusCode == System.Net.HttpStatusCode.BadRequest)
            {
                var content = await response.GetContentAsStringAsync().ConfigureAwait(false);
                return GetTokenResponseFromContent(response.StatusCode, content);
            }
            else
            {
                return GetTokenResponse(response.StatusCode, response.ReasonPhrase);
            }
        }

        #endregion

        #endregion
        #region Constructors

        internal AuthenticationClient(IHttpClient client, System.Uri address, string clientId, string clientSecret, ClientAuthenticationStyle authenticationStyle)
        {
            this.Client = client;
            this.Address = address;
            this.ClientId = clientId;
            this.ClientSecret = clientSecret;
            this.AuthenticationStyle = authenticationStyle;
        }

        #endregion
    }
}
